#!/usr/bin/env python

__version__ = "0.1"
__date__ = "2008-12-17"
__author__ = "Fahad Al-Rashed"
__url__ = "http://code.google.com/p/atomicgtd/"

from types import *
import random, re, socket, time, urllib, httplib
import sys, codecs
import base64, md5, cgi
from datetime import *

# Configuration
baseExchangeURL = 'http://exchangeserver/owa/'
baseOutputDir = '/path/to/www/root/'
exchangeUsername = 'USERNAME'
exchangePassword = 'PASSWORD'

class ExchangeAccessError(Exception):
    pass


# Once again, standing on the shoulders of rancidbacon, aka follower@myrealbox.com
class CookieJar:

    def __init__(self):
        self._cookies = {}
        self._cookie1 = re.compile('(?:, )([\w]+=[^;]+)')
        self._cookie2 = re.compile('^([\w]+=[^;]+)')

    def extractCookies(self, response, nameFilter = None):
        cookievalue = response.getheader('Set-Cookie')
        if cookievalue:
            together = self._cookie1.findall(cookievalue)
            together[0:0] = self._cookie2.findall(response.getheader('Set-Cookie'))
            for cookie in together:
                name, value = (cookie.split("=", 1) + [""])[:2]
                if not nameFilter or name in nameFilter:
                    self._cookies[name] = value.split(";")[0]


    def addCookie(self, name, value):
        self._cookies[name] = value

    def hasCookie(self, name):
        return self._cookies.has_key(name)

    def getCookies(self):
        return {'Cookie': "; ".join(["%s=%s" % (k,v) for k,v in self._cookies.items()])}

       

class ExcService:
    """
        An abstract class to access Exchange various services. Useful to handle login and subsequent cookie dance.
    """
    def __init__(self):
        self._baseURL = baseExchangeURL
        self._basedir = baseOutputDir
        self._cookies = CookieJar()
        self._conns = {}
        self._conns['http'] = {}
        self._conns['https'] = {}
        self._url = re.compile('(http|ftp|https):\/\/([\w\-_\.]+)+([\w\-\.,@?^=%&amp;:/~\+#]*[\w\-\@?^=%&amp;/~\+#])?')
        self._reown = re.compile('<title>([\w\W]+?) - Outlook Web Access</title>')
        self._stsks = re.compile('var a_sTsks = "([\w\W]+?)";')
        self._tskhtml = re.compile('<tr title="In Folder: Tasks" id="us">([\w\W]+?)</tr>')
        self._tctsks = re.compile('tC="(\d+?)"')
        self._ertsks = re.compile('eR="(\d+?)"')
        self._srtsks = re.compile('sR="(\d+?)"')
        self._tskstr = re.compile('<td nowrap class=tskStl id=sb>([\w\W]+?)</td>')
        self._tskid = re.compile('<a tabindex="-1" href="#" id="([\w\W]+?)" t="IPM.Task"')
        self._tskcat = re.compile('<td nowrap id=tdCat>([\w\W]+?)</td>')
        self._cat = re.compile('title="([\w\W]+?)"')
        self._tskcmp = re.compile('<span id=spnTskStl class=tskCmp>')
        self._tsknote = re.compile('<textarea id=txtBdy class="w100 txtBdy" style="display:none">([\w\W]+?)</textarea>')
        self._tskatchbdy = re.compile('<span id=spnAtmt tabindex="-1"([\w\W]+?)</span>')
        self._tskatchid = re.compile('_attid="([\w\W]+?)" _level=3')
        self._tskatchfilename = re.compile('title="([\w\W]+?)"')
        self._tskatchqs = re.compile('_qs="([\w\W]+?)"')
         
        
        
    def login(self, username, password, serviceURL):
        """
        A login method which can be used to log to Exchange
        """
        base64string = base64.encodestring('%s:%s' % (username, password))[:-1]
        (p, result) = self._get_page(serviceURL, None, {"Authorization": "Basic %s" % base64string})
        
        return result
        
    def _get_page(self, url, post_data=None, headers=None):
        """
        Gets the url URL with cookies enabled. Posts post_data.
        """

        urlp = self._url.findall(url)
        urlp = urlp[0]
        if urlp[0] == 'http':
          if self._conns[urlp[0]].has_key(urlp[1]):
            conn = self._conns[urlp[0]][urlp[1]]
          else:
            conn = httplib.HTTPConnection(urlp[1])
            self._conns[urlp[0]][urlp[1]] = conn
        elif urlp[0] == 'https':
          if self._conns[urlp[0]].has_key(urlp[1]):
            conn = self._conns[urlp[0]][urlp[1]]
          else:
            conn = httplib.HTTPSConnection(urlp[1])
            self._conns[urlp[0]][urlp[1]] = conn

        header = {"Content-Type": "application/x-www-form-urlencoded", "User-Agent": "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; InfoPath.2; .NET CLR 2.0.50727; .NET CLR 3.0.04506.648; .NET CLR 3.5.21022)"}
        
        header.update(self._cookies.getCookies())
        if headers != None:
            header.update(headers)


        print '--request--------------------------------'
        print 'url:'+str(urlp[2])
        print 'postdata:'+str(post_data)
        print 'sendRequestCookies:' + str(self._cookies.getCookies())
        print 'headers:'+str(header)

        if post_data == None:
            conn.request("GET", urlp[2], post_data, header )
        else:
            conn.request("POST", urlp[2], post_data, header)
                    
        f = conn.getresponse()
        result = f.read()
        print '--response-------------------------------'
        print f.status, f.reason
        print 'headers:' + str(f.getheaders())
        
        if f.status == 302:
            self._cookies.extractCookies(f)
            self._get_page(f.getheader('location'))
        print '-----------------------------------------'
        
        self._cookies.extractCookies(f)
        return (f, result)

    def _url_quote(self, value): # This method is copyright (C) 2004, Adrian Holovaty
        """
        Helper method that quotes the given value for insertion into a query
        string. Also encodes into UTF-8 in case of
        non-ASCII characters.
        """
        value = self._encode(value)
        return urllib.quote_plus(value)

    def _encode(self, value): # This method is copyright (C) 2004, Adrian Holovaty
        """
        Helper method. Convert to UTF-8 in order to allow
        non-ASCII characters.
        """
        if isinstance(value, unicode):
            value = value.encode("utf-8")
        return value

class ExcTasks(ExcService):
    """
    Simple Tasks Exctractor
    """
    def __init__(self, username, password):
        ExcService.__init__(self)
        self.username = username
        self.password = password
        if self.username and self.password:
            self.login(username, password)
    
    def login(self, username, password):
        """
        Logs to Exchange
        """
        resultpage = ExcService.login(self, username, password, self._baseURL)
        self._ftid = self._stsks.findall(resultpage)[0]
        # strip escape seq
        self._ftid = self._ftid.replace('\\', '') 
        self._owner = self._reown.findall(resultpage)[0]
        
    
    def gettasks(self):
        """
        retrieves tasks list from exchange and creates the .tasks list
        """
        (hdl, resultpage) = self._get_page(self._baseURL + "ev.owa?oeh=1&ns=TskListView&ev=Refresh", '<params><fltr>1</fltr><st><FLVS sId="%s" tC="0" sR="1" eR="1" mL="0" sC="3" sO="0"></FLVS></st></params>' % self._ftid)
        if hdl.status != 200:
            raise ExchangeAccessError, "Request returned non OK" 
        totalcounttsks = int(self._tctsks.findall(resultpage)[0])
        endrangetsks = int(self._ertsks.findall(resultpage)[0])
        
        tsks = self._tskhtml.findall(resultpage)
        print "number of tsks: "+ str(len(tsks))
        for i in range(1, totalcounttsks, endrangetsks):
            (hdl, resultpage) = self._get_page(self._baseURL + "ev.owa?oeh=1&ns=TskListView&ev=NextPage", '<params><fltr>1</fltr><st><FLVS sId="%s" tC="%s" sR="%s" eR="%s" mL="0" sC="3" sO="0"></FLVS></st></params>' % (self._ftid, str(totalcounttsks), str(i), str(i + endrangetsks - 1)))
            tsks += self._tskhtml.findall(resultpage)
            if totalcounttsks == int(self._ertsks.findall(resultpage)[0]): break
        
        self.tasks = []
        for tskhtml in tsks:
            record = {}
            if len(self._tskcmp.findall(tskhtml)) > 0:
                record['comp'] = True
            else:
                record['comp'] = False

            record['task'] = self._tskstr.findall(tskhtml)[0]
            record['id'] = self._tskid.findall(tskhtml)[0][1:]
            
            categories = self._tskcat.findall(tskhtml)[0]
            if len(self._cat.findall(categories)) > 0:
                record['categories'] = self._cat.findall(categories)[0].split('; ')
            else:
                record['categories'] = []
            self.tasks.append(record)
        
        # add notes to each task
        for task in self.tasks:
            (hdl, resultpage) = self._get_page(self._baseURL + '?ae=Item&a=Preview&t=IPM.Task&id=%s' % self._url_quote(task['id'])) # remove the begining  'b'
            task['note'] = self._tsknote.findall(resultpage)[0]
            task['attch'] = []
            attchresult = self._tskatchbdy.findall(resultpage)
            for attch in attchresult:
                print self._tskatchid.findall(attch)
                id = self._tskatchid.findall(attch)[0]
                name = self._tskatchfilename.findall(attch)[0]
                qs = self._tskatchqs.findall(attch)

                if len(qs) > 0:
                    task['attch'].append((name, self._baseURL + '?%s&id=%s&attid0=%s&attcnt=1' % (qs[0], self._url_quote(task['id']), self._url_quote(id))))
                else:
                    task['attch'].append((name, self._baseURL + 'attachment.ashx?attach=1&id=%s&attid0=%s&attcnt=1' % (self._url_quote(task['id']), self._url_quote(id))))
                
                if hdl.status != 200:
                    raise ExchangeAccessError, "Request returned non OK" 
            
            
        self.categories = {}
        for task in self.tasks:
            for category in task['categories']:
                if not self.categories.has_key(category): self.categories[category] = []
                self.categories[category].append(task)
            
if __name__ == "__main__":
    try:
        g = ExcTasks(exchangeUsername, exchangePassword)
    except:
        print "Could not log in to Google Notebook. Make sure your username and password are correct"
        sys.exit(1)
    g.gettasks()
    dtime = datetime.utcnow().replace(microsecond=0).isoformat()+'Z'
    atomfeed = open(g._basedir + 'tasks.xml', 'wb')
    atomfeed.write(u'<?xml version="1.0" encoding="utf-8"?>\n')
    atomfeed.write(u'<feed xmlns="http://www.w3.org/2005/Atom">\n')
    atomfeed.write(u'        <title>Tasks: %s</title>\n' % g._owner)
    atomfeed.write(u'        <subtitle>Outlook tasks extracted by AtomicGTD</subtitle>\n')
    atomfeed.write(u'        <updated>%s</updated>\n' % dtime )
    atomfeed.write(u'        <author>\n')
    atomfeed.write(u'                <name>%s</name>\n' % g._owner)
    atomfeed.write(u'        </author>\n')
    atomfeed.write(u'        <id>urn:sid:%s</id>\n' % md5.new(g._owner).hexdigest())
    for category in g.categories.keys():
        cat = g.categories[category]
        catidmd5 = md5.new()
        content = ''
        for task in cat:
            if not task['comp']:
                task['note'] = task['note'].strip().replace('\n', '<br>') 
                attch = []
                for att in task['attch']:
                    attch.append('<a id=lnkAtmt href="%s">%s</a>' % (att[1], att[0]))
                content += cgi.escape(u'                   <li>%s<div style="border: 1px solid #ccc; background-color: #DDFFFF; padding: 5px 5px 5px 15px">%s%s</div></li>\n' % (task['task'].decode('utf-8'), '<br>'.join(attch), task['note'].decode('utf-8'))).encode( "utf-8" )
                catidmd5.update(task['task'])
                catidmd5.update(task['id'])
        
        # if no tasks are available under the category, skip
        if catidmd5.hexdigest() == 'd41d8cd98f00b204e9800998ecf8427e': continue
        
        atomfeed.write(u'        <entry>\n')
        atomfeed.write(u'                <title>%s</title>\n' % category)
        atomfeed.write(u'                <id>urn:sid:%s</id>\n' % catidmd5.hexdigest())
        atomfeed.write(u'                <updated>%s</updated>\n' % dtime )

        atomfeed.write(u'                <content type="html">\n')
        atomfeed.write(cgi.escape(u'                 <ul>\n'))
        atomfeed.write(content)
        atomfeed.write(cgi.escape(u'                 </ul>\n'))
        atomfeed.write(u'                </content>\n')
        
        atomfeed.write(u'        </entry>\n')

    atomfeed.write(u'</feed>\n')
